/*
 * Equirectangular projection.
 */

import {DEG, RAD, π} from "../util/consts.js";
import EQUIRECTANGULAR_SIMPLE_FRAG from "./equirectangularSimple.frag";
import EQUIRECTANGULAR_COMPLEX_FRAG from "./equirectangularComplex.frag";


/**
 * Decomposes a D3 projection's configuration into arguments for the projection.
 * @param proj a D3 projection
 * @returns {number[]} the args array for {@link equirectangularProjection}.
 */
export function equirectangularArgsFromD3(proj) {
    // TODO: add support for angle, center, clipExtent, clipAngle...
    const [λ0, φ0, γ0] = proj.rotate();
    const [x0, y0] = proj.translate();
    return [proj.scale(), λ0, φ0, γ0, x0, y0];
}

/**
 * @param {number} R radius of the sphere (i.e., scale)
 * @param {number} λ0 initial rotation (degrees)
 * @param {number} φ0 initial rotation (degrees)
 * @param {number} γ0 initial rotation (degrees)
 * @param {number} x0 translation along x axis
 * @param {number} y0 translation along y axis
 * @returns {{project: function, invert: function, webgl: function}}
 */
export function equirectangularProjection(R, λ0, φ0, γ0, x0, y0) {
    return φ0 === 0 && γ0 === 0 ?
        equirectangularProjectionSimple(R, λ0, x0, y0) :
        equirectangularProjectionComplex(R, λ0, φ0, γ0, x0, y0);
}

function equirectangularProjectionSimple(R, λ0, x0, y0) {

    λ0 *= RAD;
    const center = [x0, y0];

    /**
     * @param {number} lon latitude (degrees)
     * @param {number} lat longitude (degrees)
     * @returns {number[]} resulting [x, y] or [NaN, NaN] if the coordinates are not defined for the projection.
     */
    function project(lon, lat) {
        let λ = lon * RAD;
        let φ = lat * RAD;

        // rotate
        λ += λ0;

        // project
        const px = λ;
        const py = φ;

        // translate + scale
        const x = R * px + x0;
        const y = y0 - R * py;
        return [x, y];
    }

    /**
     * @param {number} x screen x coordinate
     * @param {number} y screen y coordinate
     * @returns {number[]} resulting [λ, φ] in degrees or [NaN, NaN] if the point is not defined for the projection.
     */
    function invert(x, y) {
        // translate + scale
        const px = (x - x0) / R;
        const py = (y0 - y) / R;

        if (Math.abs(px) <= π && Math.abs(py) <= π/2) {
            // project
            let λ = px;
            let φ = py;

            // rotate
            λ -= λ0;

            const lon = λ * DEG;
            const lat = φ * DEG;
            return [lon, lat];
        }

        return [NaN, NaN];
    }

    function webgl() {
        return {
            shaderSource: EQUIRECTANGULAR_SIMPLE_FRAG,
            uniforms: {
                u_translate: center,  // screen coords translation (x0, y0)
                u_R: R,               // scale R
                u_lon0: λ0,           // origin lon
            },
        };
    }

    return {project, invert, webgl};
}

function equirectangularProjectionComplex(R, λ0, φ0, γ0, x0, y0) {

    λ0 *= RAD;
    φ0 *= RAD;
    γ0 *= RAD;
    const cosφ0 = Math.cos(φ0);
    const sinφ0 = Math.sin(φ0);
    const cosγ0 = Math.cos(γ0);
    const sinγ0 = Math.sin(γ0);
    const center = [x0, y0];

    /**
     * @param {number} lon latitude (degrees)
     * @param {number} lat longitude (degrees)
     * @returns {number[]} resulting [x, y] or [NaN, NaN] if the coordinates are not defined for the projection.
     */
    function project(lon, lat) {
        let λ = lon * RAD;
        let φ = lat * RAD;

        // rotate. Formulas pulled from d3-geo
        λ += λ0;
        const cosφ = Math.cos(φ);
        const s = Math.cos(λ) * cosφ;
        const t = Math.sin(λ) * cosφ;
        const u = Math.sin(φ);
        const v = u * cosφ0 + s * sinφ0;
        λ = Math.atan2(t * cosγ0 - v * sinγ0, s * cosφ0 - u * sinφ0);
        φ = Math.asin(v * cosγ0 + t * sinγ0);

        // project
        const px = λ;
        const py = φ;

        // translate + scale
        const x = R * px + x0;
        const y = y0 - R * py;
        return [x, y];
    }

    /**
     * @param {number} x screen x coordinate
     * @param {number} y screen y coordinate
     * @returns {number[]} resulting [λ, φ] in degrees or [NaN, NaN] if the point is not defined for the projection.
     */
    function invert(x, y) {
        // translate + scale
        const px = (x - x0) / R;
        const py = (y0 - y) / R;

        if (Math.abs(px) <= π && Math.abs(py) <= π/2) {
            // project
            let λ = px;
            let φ = py;

            // rotate. Formulas pulled from d3-geo
            const cosφ = Math.cos(φ);
            const s = Math.cos(λ) * cosφ;
            const t = Math.sin(λ) * cosφ;
            const u = Math.sin(φ);
            const v = u * cosγ0 - t * sinγ0;
            λ = Math.atan2(t * cosγ0 + u * sinγ0, s * cosφ0 + v * sinφ0);
            φ = Math.asin(v * cosφ0 - s * sinφ0);
            λ -= λ0;

            const lon = λ * DEG;
            const lat = φ * DEG;
            return [lon, lat];
        }

        return [NaN, NaN];
    }

    function webgl() {
        return {
            shaderSource: EQUIRECTANGULAR_COMPLEX_FRAG,
            uniforms: {
                u_translate: center,  // screen coords translation (x0, y0)
                u_R: R,               // scale R
                u_lon0: λ0,           // origin lon
                u_sinlat0: sinφ0,     // sin(φ0)
                u_coslat0: cosφ0,     // cos(φ0)
                u_singam0: sinγ0,     // sin(γ0)
                u_cosgam0: cosγ0,     // cos(γ0)
            },
        };
    }

    return {project, invert, webgl};
}
